﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel.Composition;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;

using iStudio.Structure.Engine;
using iStudio.Structure.Control.Audio.Editor;
using iStudio.Structure.Wave;

namespace iStudio.Editor.BasicRenderer
{
    /// <summary>
    /// Renders basic waveform view, timeline and progress
    /// </summary>
    [Export (typeof (EditorRenderer))]
    public class BasicRenderer : EditorRenderer
    {
        #region Fields

        //
        // Track list render
        //

        private readonly List<BasicCachedTrack> _trackListCache;
        private double _renderedPosition;

        //
        // Render timeline
        //

        private readonly List<Line> _timeLineTicks;
        private readonly Rectangle _timelineBackgroundRegion;
        private readonly List<TextBlock> _timestampTextBlocks;

        private Border _timelineBorder;
        private double _channelLength;

        //
        // Progress Indicator
        //

        private readonly Path _progressIndicator;
        private readonly Line _progressLine;

        #endregion

        #region Constants
        private const int MajorTickHeight = 15;
        private const int MinorTickHeight = 8;
        private const int TimeStampMargin = 8;
        private const double Epsilon = 0.00001;
        private const double RenderChannelHeight = 40.0;
        private const int IndicatorTriangleWidth = 4;
        #endregion

        #region Constructor
        public BasicRenderer ()
        {
            _trackListCache = new List<BasicCachedTrack> ();

            //
            // Setup class variables
            //

            _timeLineTicks = new List<Line> ();
            _timelineBackgroundRegion = new Rectangle ();
            _timestampTextBlocks = new List<TextBlock> ();

            _progressIndicator = new Path ();
            _progressLine = new Line ();
        }
        #endregion

        protected override void RegisterTrackEditorEvents ()
        {
            
        }

        protected override void RegisterAudioEngineEvents()
        {
            //
            // Register changed wave track list
            //

            AudioEngine.Instance.Project.TrackList.CollectionChanged += TrackListOnCollectionChanged;
        }

        private void TrackListOnCollectionChanged (object sender, NotifyCollectionChangedEventArgs notifyCollectionChangedEventArgs)
        {
            //
            // Implement adding, removing and changing TrackList
            //

            if (notifyCollectionChangedEventArgs.Action == NotifyCollectionChangedAction.Add)
            {
                BasicCachedTrack track = new BasicCachedTrack ((WaveTrack)notifyCollectionChangedEventArgs.NewItems[0], EditorCore);
            
                //
                // Track position modification
                //

                if (_renderedPosition > 0)
                {
                    track.SetCanvasProperty (EditorCanvasProperty.MarginTop, _renderedPosition);
                }

                _renderedPosition += ((WaveTrack) notifyCollectionChangedEventArgs.NewItems[0]).WaveFormat.Channels * RenderChannelHeight;

                _trackListCache.Add (track);
            }
        }

        /// <summary>
        /// Handles sound player property changes
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        protected override void SoundPlayerPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
        {
            //
            // If channel length changes we need to reconstruct timeline
            // We expect tracks to be already rendered
            //

            switch (e.PropertyName)
            {
                case "Project":
                    UpdateTimeline (SoundPlayer.Project.Length);
                    UpdateProgressIndicator();

                    break;
                case "ChannelPosition":
                    UpdateProgressIndicator ();

                    break;
            }
        }

        /// <summary>
        /// Render size is changed of the control, certain objects might need to be changed
        /// </summary>
        /// <param name="sizeInfo"></param>
        public override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
        {
            _progressLine.Y2 = sizeInfo.NewSize.Height;
        }

        /// <summary>
        /// Function updates progress indicator
        /// </summary>
        private void UpdateProgressIndicator ()
        {
            if ((SoundPlayer == null) || (EditorCore == null) )
            {
                return;
            }

            //
            // Check if canvas progress is defined on editor core
            //

            if (EditorCore.ProgressCanvas.Children.Count == 0)
            {
                CreateProgressIndicator ();
            }

            //
            // Change position of the indicator
            //

            double xLocation = 0.0d;

            if (Math.Abs(SoundPlayer.Project.Length - 0) > Epsilon)
            {
                //
                // Calculate current progress of play
                //

                //
                // xLocation is calculated based on sample rate, point thickness and frame size
                //

                xLocation = SoundPlayer.ChannelPosition * SoundPlayer.Project.Format.SampleRate * 0.3 / 128;
            }

            Canvas.SetLeft(_progressLine, xLocation);
            Canvas.SetLeft(_progressIndicator, xLocation);
        }

        /// <summary>
        /// Function setups progress indicator
        /// </summary>
        private void CreateProgressIndicator ()
        {
            _progressLine.X1 = 0;
            _progressLine.X2 = 0;
            _progressLine.Y1 = 0;
            _progressLine.Y2 = EditorCore.ProgressCanvas.RenderSize.Height;
            _progressLine.Stroke = EditorCore.ProgressBarBrush;

            DoubleCollection dashes = new DoubleCollection
                                          {
                                              5,
                                              5
                                          };
            _progressLine.StrokeDashArray = dashes;
            _progressLine.StrokeDashCap = PenLineCap.Round;

            PolyLineSegment indicatorPolySegment = new PolyLineSegment();
            indicatorPolySegment.Points.Add(new Point(0, EditorCore.TimelineCanvas.RenderSize.Height));
            indicatorPolySegment.Points.Add(new Point(0 - IndicatorTriangleWidth, 8));
            indicatorPolySegment.Points.Add(new Point(0 - IndicatorTriangleWidth, 3));
            indicatorPolySegment.Points.Add(new Point(0 + IndicatorTriangleWidth, 3));
            indicatorPolySegment.Points.Add(new Point(0 + IndicatorTriangleWidth, 8));
            indicatorPolySegment.Points.Add(new Point(0, EditorCore.TimelineCanvas.RenderSize.Height));

            PathGeometry indicatorGeometry = new PathGeometry();
            PathFigure indicatorFigure = new PathFigure();
            indicatorFigure.Segments.Add(indicatorPolySegment);
            indicatorGeometry.Figures.Add(indicatorFigure);

            _progressIndicator.Opacity = 0.7;

            _progressIndicator.Data = indicatorGeometry;
            _progressIndicator.Fill = EditorCore.ProgressBarBrush;

            EditorCore.TimelineCanvas.Children.Add(_progressIndicator);
            EditorCore.ProgressCanvas.Children.Add(_progressLine);
        }

        /// <summary>
        /// Function rerenders timeline based on current zoom and project length
        /// </summary>
        /// <param name="channelLength"></param>
        private void UpdateTimeline (double channelLength)
        {
            //
            // Just in case check for both required objects and if channel length even changed
            //

            if ( (SoundPlayer == null) || (EditorCore == null) || (Math.Abs (channelLength - _channelLength) < Epsilon) )
            {
                return;
            }

            //
            // If we have at least one valid track on list and if we did not setup the elements yet
            //

            if ((SoundPlayer.Project.TrackList.Count > 0) && (EditorCore.TimelineCanvas.Children.Count == 0))
            {
                _timelineBorder = EditorCore.TimelineBorder;

                _timelineBorder.BorderThickness = new Thickness (0, 0, 0, 1);

                _timelineBackgroundRegion.Width = EditorCore.TimelineCanvas.RenderSize.Width;
                _timelineBackgroundRegion.Height = EditorCore.TimelineCanvas.RenderSize.Height;

                EditorCore.TimelineCanvas.Children.Add (_timelineBackgroundRegion);
            }

            double bottomLoc = EditorCore.TimelineCanvas.RenderSize.Height - 1;

            //
            // TODO: Setup correct zoom
            //

            double minorTickDuration = 0.50d; // Major tick = 5 seconds, Minor tick = 1.00 second
            double majorTickDuration = 2.00d;

            //if (SoundPlayer.ChannelLength >= 60.0d)
            //{
            //    minorTickDuration = 1.00d;
            //    majorTickDuration = 5.0d;
            //}
            //else if (SoundPlayer.ChannelLength >= 120.0d) // Major tick = 1 minute, Minor tick = 15 seconds.
            //{
            //    minorTickDuration = 15.0d;
            //    majorTickDuration = 60.0d;
            //}
            //else if (SoundPlayer.ChannelLength >= 60.0d) // Major tick = 30 seconds, Minor tick = 5.0 seconds.
            //{
            //    minorTickDuration = 5.0d;
            //    majorTickDuration = 30.0d;
            //}
            //else if (SoundPlayer.ChannelLength >= 30.0d) // Major tick = 10 seconds, Minor tick = 2.0 seconds.
            //{
            //    minorTickDuration = 2.0d;
            //    majorTickDuration = 10.0d;
            //}

            if (SoundPlayer.Project.Length < minorTickDuration)
            {
                return;
            }

            int minorTickCount = (int)(SoundPlayer.Project.Length / minorTickDuration);

            for (int i = 1; i <= minorTickCount; i++)
            {
                Line timelineTick = new Line
                {
                    Stroke = EditorCore.TimelineTickBrush,
                    StrokeThickness = 1.0d
                };
                double xLocation = ((i * minorTickDuration) / SoundPlayer.Project.Length) * EditorCore.TimelineCanvas.RenderSize.Width;

                bool drawTextBlock = false;
                double lastTimestampEnd;
                if (_timestampTextBlocks.Count != 0)
                {
                    TextBlock lastTextBlock = _timestampTextBlocks[_timestampTextBlocks.Count - 1];
                    lastTimestampEnd = lastTextBlock.Margin.Left + lastTextBlock.ActualWidth;
                }
                else
                {
                    lastTimestampEnd = 0;
                }

                if (xLocation > lastTimestampEnd + TimeStampMargin)
                {
                    drawTextBlock = true;
                }

                // Flag that we're at the end of the timeline such 
                // that there is not enough room for the text to draw.
                bool isAtEndOfTimeline = (EditorCore.TimelineCanvas.RenderSize.Width - xLocation < 28.0d);

                if (Math.Abs(i % (majorTickDuration / minorTickDuration) - 0) < Epsilon)
                {
                    timelineTick.X1 = xLocation;
                    timelineTick.Y1 = bottomLoc;
                    timelineTick.X2 = xLocation;
                    timelineTick.Y2 = bottomLoc - MajorTickHeight;
                }
                else
                {
                    timelineTick.X1 = xLocation;
                    timelineTick.Y1 = bottomLoc;
                    timelineTick.X2 = xLocation;
                    timelineTick.Y2 = bottomLoc - MinorTickHeight;
                }

                if (drawTextBlock)
                {
                    if (isAtEndOfTimeline)
                    {
                        continue;
                    }

                    TimeSpan timeSpan = TimeSpan.FromSeconds(i * minorTickDuration);
                    TextBlock timestampText;

                    if (Math.Abs(i % (majorTickDuration / minorTickDuration) - 0) < Epsilon)
                    {
                        timestampText = new TextBlock
                        {
                            Margin = new Thickness(xLocation + 2, 0, 0, 0),
                            FontFamily = EditorCore.FontFamily,
                            FontStyle = EditorCore.FontStyle,
                            FontWeight = FontWeights.SemiBold,
                            FontStretch = EditorCore.FontStretch,
                            FontSize = EditorCore.FontSize,
                            Foreground = EditorCore.Foreground,
                            Text = (timeSpan.TotalHours >= 1.0d) ? string.Format(@"{0:hh\:mm\:ss}", timeSpan) : string.Format(@"{0:mm\:ss}", timeSpan)
                        };
                    }
                    else
                    {
                        timestampText = new TextBlock
                        {
                            Margin = new Thickness(xLocation - 15, 4, 0, 0),
                            FontFamily = EditorCore.FontFamily,
                            FontStyle = EditorCore.FontStyle,
                            FontWeight = EditorCore.FontWeight,
                            FontStretch = EditorCore.FontStretch,
                            FontSize = EditorCore.FontSize - 1,
                            Foreground = EditorCore.Foreground,
                            Text = (timeSpan.TotalHours >= 1.0d) ? string.Format(@"{0:hh\:mm\:ss}", timeSpan) : string.Format(@"{0:mm\:ss\.ff}", timeSpan)
                        };
                    }

                    _timestampTextBlocks.Add(timestampText);
                    EditorCore.TimelineCanvas.Children.Add(timestampText);
                    EditorCore.UpdateLayout(); // Needed so that we know the width of the textblock.
                }
                else // If still on the text block, draw a minor tick mark instead of a major.
                {
                    timelineTick.X1 = xLocation;
                    timelineTick.Y1 = bottomLoc;
                    timelineTick.X2 = xLocation;
                    timelineTick.Y2 = bottomLoc - MinorTickHeight;
                }

                _timeLineTicks.Add(timelineTick);
                EditorCore.TimelineCanvas.Children.Add(timelineTick);
            }

            _channelLength = channelLength;
        }

        /*
        // TODO: Fix optimization of Track Editor bitmap cache
        // The Code below is merely an optimization for track editor bitmap cache, yet to implement
        //

        private void UpdateWaveformCacheScaling()
        {
            if (_waveformCanvas == null)
            {
                return;
            }

            BitmapCache waveformCache = (BitmapCache)_waveformCanvas.CacheMode;

            if (EditorCore.AutoScaleWaveformCache)
            {
                double totalTransformScale = GetTotalTransformScale();
                if (waveformCache != null && Math.Abs(waveformCache.RenderAtScale - totalTransformScale) > Epsilon)
                {
                    waveformCache.RenderAtScale = totalTransformScale;
                }
            }
            else
            {
                waveformCache.RenderAtScale = 1.0d;
            }
        }

        private double GetTotalTransformScale()
        {
            double totalTransform = 1.0d;
            DependencyObject currentVisualTreeElement = EditorCore;
            do
            {
                Visual visual = currentVisualTreeElement as Visual;
                if (visual != null)
                {
                    Transform transform = VisualTreeHelper.GetTransform(visual);

                    // This condition is a way of determining if it
                    // was a uniform scale transform. Is there some better way?
                    if ((transform != null) &&
                        (Math.Abs(transform.Value.M12 - 0) < Epsilon) &&
                        (Math.Abs(transform.Value.M21 - 0) < Epsilon) &&
                        (Math.Abs(transform.Value.OffsetX - 0) < Epsilon) &&
                        (Math.Abs(transform.Value.OffsetY - 0) < Epsilon) &&
                        (Math.Abs(transform.Value.M11 - transform.Value.M22) < Epsilon))
                    {
                        totalTransform *= transform.Value.M11;
                    }
                }
                currentVisualTreeElement = VisualTreeHelper.GetParent(currentVisualTreeElement);
            }
            while (currentVisualTreeElement != null);

            return totalTransform;
        }*/
    }
}
